<?php

/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2019/3/12 0012
 * Time: 22:18
 */

//https://blog.csdn.net/qq_33344121/article/details/79982555

/**
 * Class webSocketServer
 * 需求描述
 * 1.我们主要做的事就是解析客户端的消息，然后根据不同的type执行不同的函数
 * 2.然后我们需要一个给同一个聊天室里的人推送消息的函数，和给所用客户端推送消息的函数用来推送在线人数
 */
require 'Log.php';

class WebSocketServer
{
    private $sockets;//所有socket连接池包括服务端socket
    private $users; //所有连接用户
    private $server;//服务端socket

    private $chatRooms;//所有聊天室

    private $waitUsers = [];//等待匹配的用户


    public function __construct($ip, $port)
    {
        $this->server = socket_create(AF_INET, SOCK_STREAM, IPPROTO_IP) or die('socket_create fail' . socket_strerror(socket_last_error()));
        $this->sockets = [$this->server];
        $this->users = [];
        //绑定到socket
        socket_bind($this->server, $ip, $port) or die('socket_bind fail' . socket_strerror(socket_last_error()));
        //开始监听
        socket_listen($this->server, 3) or die('socket_listen fail' . socket_strerror(socket_last_error()));
        echo "[*]Listening:" . $ip . ":" . $port . "\n";
    }

    //数据库连接
    private function connect_db()
    {
        $con = mysqli_connect("localhost", "root", "Abc123!Abc", "chatroom") or die("连接错误: " . mysqli_connect_error());;
        return $con;
    }

    public function run()
    {
        $write = null;
        $except = null;
        while (true) {
            $active_sockets = $this->sockets;

            socket_select($active_sockets, $write, $except, null);

            foreach ($active_sockets as $active_socket) {

                if ($active_socket == $this->server) {
                    echo "Waiting for Connection...\n";
                    //服务端socket可读说明有新的客户端连接
                    $user_socket = socket_accept($this->server) or die('socket_accept fail' . socket_strerror(socket_last_error())); //成功时返回新套接字资源，错误时返回false
                    $key = uniqid();
                    $this->sockets[] = $user_socket;
                    $this->users[$key] = [
                        'socket' => $user_socket,
                        'handshake' => false //是否完成webSocket握手
                    ];
                } else {
                    //用户socket可读
                    $buffer = '';
                    $bytes = socket_recv($active_socket, $buffer, 1024, 0);//socket_recv() 返回一个数字，表示接收到的字节数。如果发生了错误，则返回 FALSE
                    $key = $this->findUserBySocket($active_socket); //通过socket在users数组中找出user
                    if ($bytes == 0) {
                        //没有数据 关闭连接
                        $this->close($active_socket);
                    } else {
                        //没有握手就先握手
                        if ($this->users[$key]['handshake'] == false) {
                            $this->handshake($key, $buffer);
                            //握手成功后推送在线人数
                            Log::connect($active_socket);
                            $this->pushMsgForAll([
                                'type' => 6,
                                'data' => [
                                    'users' => count($this->sockets) - 1
                                ]
                            ]);
                        } else {
                            //握手成功后解析消息 websocket协议有自己的消息格式
                            //解码和编码的过程是固定的
                            //握手后 解析消息
                            $this->parseMsg($buffer, $active_socket);
                        }
                    }
                }
            }

        }
    }

    /**
     * @param $roomId :房间ID
     * @param $msg :发送的消息内容
     * @param null $client_socket :客户端
     * @description:同一个聊天室里的用户 广播消息
     */
    private function pushMsgForRoom($roomId, $msg, $client_socket = null)
    {
        if (!is_null($client_socket)) {
            // 当前用户存在 只把消息推送给其他两个人
            $msg = $this->msgEncode($msg);
            foreach ($this->chatRooms[$roomId] as $user) {
                //不发给自己
                if ($user !== $client_socket) {
                    socket_write($user, $msg, strlen($msg));
                }
            }
        } else {
            //将消息推送给房间所有人
            $type = $msg['type'];
            if ($type == 1) {
                //匹配成功消息推送
                //随机三个用户信息
                $users = $this->getRandRole();
                foreach ($this->chatRooms[$roomId] as $key => $user) {
                    $data = [
                        'type' => 1,
                        'data' => [
                            'room_id' => $roomId,
                            'index' => $key,
                            'users' => $users
                        ]
                    ];
                    $data = $this->msgEncode(json_encode($data));
                    socket_write($user, $data, strlen($data));
                }
            } else {
                //2 发送消息 3离开房间
                $msg = $this->msgEncode(json_encode($msg));
                foreach ($this->chatRooms[$roomId] as $user) {
                    socket_write($user, $msg, strlen($msg));
                }
            }
        }
    }

    //随机三个角色
    private function getRandRole()
    {
        $db = $this->connect_db();
        $sql = "SELECT name,url FROM roles";
        $result = mysqli_query($db, $sql);
        $roles = mysqli_fetch_all($result, MYSQLI_ASSOC);
        mysqli_free_result($result);
        mysqli_close($db);
        $randKeys = array_rand($roles, 3);
        $users = array();
        foreach ($randKeys as $key) {
            $users[] = array(
                'a_url' => $roles[$key]['url'],
                'nickname' => $roles[$key]['name']
            );
        }
        return $users;
    }

    /**
     * @param array $msg
     * @param null $client_socket
     * @description:广播消息 向所有在线人发消息
     */
    public function pushMsgForAll(array $msg, $client_socket = null)
    {
        $msg = $this->msgEncode(json_encode($msg));
        foreach ($this->sockets as $socket) {
            //除了 服务器和自己 都发送
            if ($socket != $this->server and $socket != $client_socket) {
                socket_write($socket, $msg, strlen($msg));
            }
        }
    }

    /**
     * @param $buffer
     * @param $socket : 连接成功的客户端
     * @description:根据type 具体操作
     */
    private function parseMsg($buffer, $socket)
    {
        $msg = json_decode($this->msgDecode($buffer), true);
        switch ($msg['type']) {
            case 0:
                //进行匹配
                $this->match($socket);
                break;
            case 1:
                //取消匹配
                $this->dismatch($socket);
            case 2:
                //发送消息
                $room_id = $msg['data']['root_id'];
                if (array_key_exists($room_id, $this->chatRooms)) {
                    $data = [
                        'type' => 2,
                        'data' => [
                            'msg' => $msg['data']['msg'],
                            'user' => $msg['data']['user']
                        ]
                    ];
                    $this->pushMsgForRoom($room_id, $data, $socket);
                }
                break;
            case 3:
                //离开房间
                $room_id = $msg['data']['root_id'];
                $name = $msg['data']['name'];
                $this->leaveRoom($room_id, $socket, $name);
                break;
            default:
                break;
        }
    }

    //进行匹配
    private function match($socket)
    {
        $userCount = count($this->waitUsers);
        if ($userCount >= 2) {
            //匹配成功
            $rands = array_rand($this->waitUsers, 2);
            $user_1 = $this->waitUsers[$rands[0]];
            $user_2 = $this->waitUsers[$rands[1]];
            $this->disMatch($user_1);
            $this->disMatch($user_2);
            $roomId = uniqid();//创建唯一房间号
            $this->chatRooms[$roomId] = [$socket, $user_1, $user_2];
            //推送消息 创建房间成功
            $msg = [
                'type' => 1,//1 匹配成功
                'data' => [
                    'room_id' => $roomId,
                    'msg' => '匹配成功'
                ]
            ];
            $this->pushMsgForRoom($roomId, $msg);
        } else {
            //等待匹配...
            $this->waitUsers[] = $socket;
            $msg = [
                'type' => 3,
                'data' => [
                    'msg' => '等候匹配'
                ]
            ];
            $msg = $this->msgEncode(json_encode($msg));
            socket_write($socket, $msg, strlen($msg));
        }

    }

    //取消匹配
    private function disMatch($socket)
    {
        foreach ($this->waitUsers as $k => $user) {
            if ($socket == $user) {
                unset($this->waitUsers[$k]);
                break;
            }
        }
        $msg = array(
            'type' => 5,//普通消息
            'data' => [
                'msg' => '取消匹配成功'
            ]
        );
        $msg = $this->msgEncode(json_encode($msg));
        socket_write($socket, $msg, strlen($msg));
    }

    /**
     * @param $roomId
     * @param $socket
     * @param string $name
     * @description: 离开房间
     */
    private function leaveRoom($roomId, $socket, $name = '有人')
    {
        foreach ($this->chatRooms[$roomId] as $key => $user) {
            if ($user == $socket) {
                unset($this->chatRooms[$roomId][$key]);
                break;
            }
        }
        if (count($this->chatRooms[$roomId]) == 0) {
            //所有人都离开后 清除房间
            unset($this->chatRooms[$roomId]);
        } else {
            $msg = [
                'type' => 1,//1 匹配成功
                'data' => [
                    'msg' => $name . '离开了房间'
                ]
            ];
            $this->pushMsgForRoom($roomId, $msg, $socket);
        }
    }

    //通过socket在users数组中找出user
    private function findUserBySocket($socket)
    {
        foreach ($this->users as $key => $user) {
            if ($socket == $user['socket']) {
                return $key;
            }
        }
        return -1;
    }

    //解除连接
    private function close($socket)
    {
        Log::disconnet($socket);
        //判断是否在聊天室
        foreach ($this->chatRooms as $roomId => $room) {
            if (in_array($socket, $room)) {
                $this->leaveRoom($roomId, $socket, '有人');
                break;
            }
        }
        //判断是否在等待队伍中
        if (in_array($socket, $this->waitUsers)) {
            $this->dismatch($socket);
        }
        $key = $this->findUserBySocket($socket);
        unset($this->users[$key]);
        foreach ($this->sockets as $k => $v) {
            if ($v == $socket)
                unset($this->sockets[$k]);
        }
        //socket_shutdown() 这个函数允许你关闭读、写、或指定的socket
        socket_shutdown($socket);
        socket_close($socket);
        //推送在线人数
        $this->pushMsgForAll([
            'type' => 6,
            'data' => [
                'users' => count($this->sockets) - 1
            ]
        ]);
    }

    private function handshake($userSocketKey, $buffer)
    {
        //截取Sec-WebSocket-Key的值并加密
        $buf = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
        $key = trim(substr($buf, 0, strpos($buf, "\r\n")));
        $new_key = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); //Kfh9QIsMVZcl6xEPYxPHzW8SZ8w=
        //按照协议组合信息进行返回
        $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
        $new_message .= "Upgrade: websocket\r\n";
        $new_message .= "Sec-WebSocket-Version: 13\r\n";
        $new_message .= "Connection: Upgrade\r\n";
        $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
        socket_write($this->users[$userSocketKey]['socket'], $new_message, strlen($new_message)) or die('socket_write 失败原因是' . socket_strerror(socket_last_error()) . "\n");
        //对已经握手的client做标志
        $this->users[$userSocketKey]['handshake'] = true;
        return true;
    }

    //编码 把消息打包成websocket协议支持的格式
    private function msgEncode($buffer)
    {
        $len = strlen($buffer);
        if ($len <= 125) {
            return "\x81" . chr($len) . $buffer;
        } else if ($len <= 65535) {
            return "\x81" . chr(126) . pack("n", $len) . $buffer;
        } else {
            return "\x81" . char(127) . pack("xxxxN", $len) . $buffer;
        }
    }

    //解码 解析websocket数据帧
    private function msgDecode($buffer)
    {
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else if ($len === 127) {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        } else {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        return $decoded;
    }
}
